package com.google.code.jtasks.bugzilla;

import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.Duration;

import com.google.code.jtasks.adapters.IRepositoryMetadataProvider;
import com.google.code.jtasks.bugzilla.fields.BugzillaStandardField;
import com.google.code.jtasks.bugzilla.proxy.bug.BugCommentsProxy.Comment;
import com.google.code.jtasks.bugzilla.proxy.bug.BugHistoryProxy.Change;
import com.google.code.jtasks.bugzilla.proxy.bug.BugHistoryProxy.HistoryEntry;
import com.google.code.jtasks.connection.TaskRepoId;
import com.google.code.jtasks.model.IMappedTask;
import com.google.code.jtasks.model.ITaskWorkEntry;
import com.google.code.jtasks.model.Priority;
import com.google.code.jtasks.model.Resolution;
import com.google.code.jtasks.model.TaskId;
import com.google.code.jtasks.model.UnsupportedTaskPropertyException;
import com.google.inject.Guice;

/**
 * This {@link IMappedTask} implementation maps Bugzilla {@link BugContainer}s into the standard
 * {@link IMappedTask} format.
 */
public class BugzillaMappedTask implements IMappedTask
{
	private static final long serialVersionUID = -3594530077480806192L;

	/**
	 * <p>
	 * This {@link IRepositoryMetadataProvider} is used to look up enum values that aren't in the
	 * {@link BugContainer}, e.g. {@link Priority#getLevel()}.
	 * </p>
	 * <p>
	 * This is likely not {@link Serializable} by default; we're counting on {@link Guice} to wrap it in a
	 * {@link Serializable} proxy.
	 * </p>
	 */
	private final IRepositoryMetadataProvider repoMetadataProvider;

	/**
	 * Stores all the information retrieved from the Bugzilla server about the bug.
	 */
	private final BugContainer bugContainer;

	/**
	 * We cache this at construction mostly to make debugging a bit easier-- nice to have this as a top-level
	 * field that can be inspected.
	 */
	private final TaskId id;

	/**
	 * Constructs a new {@link BugzillaMappedTask} instance that will act as a standardizing proxy for the
	 * specified {@link BugContainer}.
	 * 
	 * @param bugContainer
	 *            the {@link BugContainer} that this {@link BugzillaMappedTask} will map
	 */
	public BugzillaMappedTask(IRepositoryMetadataProvider repoMetadataProvider, BugContainer bugContainer)
	{
		this.repoMetadataProvider = repoMetadataProvider;
		this.bugContainer = bugContainer;

		this.id = new TaskId(bugContainer.getBug().getId().toString());
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getId()
	 */
	@Override
	public TaskId getId()
	{
		return id;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getUrl()
	 */
	@Override
	public URL getUrl()
	{
		// TODO should implement this field
		// TODO Doesn't mean what I thought it meant. Actually just a "see also" kind of URL.
		throw new UnsupportedTaskPropertyException(BugzillaTaskSystemAdapter.class, TaskField.URL);
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getSummary()
	 */
	@Override
	public String getSummary()
	{
		return bugContainer.getBug().getSummary();
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getOwner()
	 */
	@Override
	public String getOwner()
	{
		return bugContainer.getBug().getCreator();
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getResolution()
	 */
	@Override
	public Resolution getResolution()
	{
		TaskRepoId taskRepoId = bugContainer.getTaskRepoId();
		BugzillaRepositoryData repoData = (BugzillaRepositoryData) repoMetadataProvider
				.getRepositoryMetadata(taskRepoId).getRepositoryData();

		String resolutionLabel = bugContainer.getBug().getResolution();
		Resolution resolution = repoData.findResolution(resolutionLabel);

		// TODO better exception
		if (resolution == null)
			throw new IllegalStateException("Missing resolution: " + resolutionLabel);

		return resolution;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getPriority()
	 */
	@Override
	public Priority getPriority()
	{
		TaskRepoId taskRepoId = bugContainer.getTaskRepoId();
		BugzillaRepositoryData repoData = (BugzillaRepositoryData) repoMetadataProvider
				.getRepositoryMetadata(taskRepoId).getRepositoryData();

		String priorityLabel = bugContainer.getBug().getPriority();
		Priority priority = repoData.findPriority(priorityLabel);

		// TODO better exception
		if (priority == null)
			throw new IllegalStateException("Missing priority: " + priorityLabel);

		return priority;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getProduct()
	 */
	@Override
	public String getProduct()
	{
		return bugContainer.getBug().getProduct();
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getComponent()
	 */
	@Override
	public String getComponent()
	{
		return bugContainer.getBug().getComponent();
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getTotalWorkEstimate()
	 */
	@Override
	public Duration getTotalWorkEstimate()
	{
		Double timeInFractionalHours = bugContainer.getBug().getEstimated_time();
		if (timeInFractionalHours != null)
		{
			long timeInSeconds = Math.round(timeInFractionalHours * DateTimeConstants.SECONDS_PER_HOUR);
			Duration time = Duration.standardSeconds((int) timeInSeconds);
			return time;
		}
		else
			return null;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getTimeWorked()
	 */
	@Override
	public Duration getTimeWorked()
	{
		Duration totalTimeWorked = Duration.ZERO;
		for (ITaskWorkEntry workEntry : getWorkEntries())
			totalTimeWorked = totalTimeWorked.plus(workEntry.getTimeWorked());
		return totalTimeWorked;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getTags()
	 */
	@Override
	public Set<String> getTags()
	{
		Set<String> tags = new HashSet<String>();
		for (String tag : bugContainer.getBug().getKeywords())
			tags.add(tag);
		return tags;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getComments()
	 */
	@Override
	public List<ITaskComment> getComments()
	{
		List<ITaskComment> mappedComments = new ArrayList<ITaskComment>();
		for (Comment proxyComment : bugContainer.getComments())
		{
			BugzillaTaskComment mappedComment = new BugzillaTaskComment(proxyComment);
			mappedComments.add(mappedComment);
		}

		return mappedComments;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getWorkEntries()
	 */
	@Override
	public List<ITaskWorkEntry> getWorkEntries()
	{
		TaskRepoId taskRepoId = bugContainer.getTaskRepoId();
		BugzillaRepositoryData repoData = (BugzillaRepositoryData) repoMetadataProvider
				.getRepositoryMetadata(taskRepoId).getRepositoryData();

		List<ITaskWorkEntry> mappedEntries = new ArrayList<ITaskWorkEntry>();
		for (HistoryEntry proxyEntry : bugContainer.getHistoryEntries())
		{
			if (!BugzillaTaskWorkEntry.containsWorkEntry(repoData, proxyEntry))
				continue;

			BugzillaTaskWorkEntry mappedEntry = new BugzillaTaskWorkEntry(proxyEntry);
			mappedEntries.add(mappedEntry);
		}

		return mappedEntries;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getBlocks()
	 */
	@Override
	public Set<TaskRelationship> getBlocks()
	{
		Set<TaskRelationship> blockedTaskRelationships = new HashSet<TaskRelationship>();
		for (Integer blockedBugId : bugContainer.getBug().getBlocks())
		{
			TaskId blockedTaskId = new TaskId(blockedBugId.toString());
			TaskRelationship blockedBugRelationship = new TaskRelationship(getId(), blockedTaskId);
			blockedTaskRelationships.add(blockedBugRelationship);
		}
		return blockedTaskRelationships;
	}

	/**
	 * @see com.google.code.jtasks.model.IMappedTask#getDependsOn()
	 */
	@Override
	public Set<TaskRelationship> getDependsOn()
	{
		Set<TaskRelationship> blockingTaskRelationships = new HashSet<TaskRelationship>();
		for (Integer blockingBugId : bugContainer.getBug().getDepends_on())
		{
			TaskId blockingTaskId = new TaskId(blockingBugId.toString());
			TaskRelationship blockingBugRelationship = new TaskRelationship(getId(), blockingTaskId);
			blockingTaskRelationships.add(blockingBugRelationship);
		}
		return blockingTaskRelationships;
	}

	/**
	 * The {@link ITaskComment} implementation used for all {@link BugzillaMappedTask}s.
	 */
	private static final class BugzillaTaskComment implements ITaskComment
	{
		private Comment commentProxy;

		/**
		 * Constructs a new {@link BugzillaTaskComment} instance.
		 * 
		 * @param commentProxy
		 *            the {@link Comment} proxy instance that this {@link BugzillaTaskComment} represents
		 */
		public BugzillaTaskComment(Comment commentProxy)
		{
			this.commentProxy = commentProxy;
		}

		/**
		 * @see com.google.code.jtasks.model.IMappedTask.ITaskComment#getAuthor()
		 */
		@Override
		public String getAuthor()
		{
			return commentProxy.getCreator();
		}

		/**
		 * @see com.google.code.jtasks.model.IMappedTask.ITaskComment#getCreationTime()
		 */
		@Override
		public DateTime getCreationTime()
		{
			return new DateTime(commentProxy.getTime());
		}

		/**
		 * @see com.google.code.jtasks.model.IMappedTask.ITaskComment#getText()
		 */
		@Override
		public String getText()
		{
			return commentProxy.getText();
		}
	}

	/**
	 * The {@link ITaskWorkEntry} implementation used for all {@link BugzillaMappedTask}s.
	 */
	private static final class BugzillaTaskWorkEntry implements ITaskWorkEntry
	{
		private HistoryEntry proxyEntry;

		/**
		 * Returns <code>true</code> if the specified {@link HistoryEntry} contains a {@link Change} for
		 * {@link BugzillaStandardField#HOURS_WORKED}, <code>false</code> if it does not.
		 * 
		 * @param repoData
		 *            the {@link BugzillaRepositoryData} for the repository the {@link HistoryEntry} was
		 *            retrieved from
		 * @param historyEntry
		 *            the {@link HistoryEntry} to check
		 * @return <code>true</code> if the specified {@link HistoryEntry} contains a {@link Change} for
		 *         {@link BugzillaStandardField#HOURS_WORKED}, <code>false</code> if it does not
		 */
		public static boolean containsWorkEntry(BugzillaRepositoryData repoData, HistoryEntry historyEntry)
		{
			for (Change change : historyEntry.getChanges())
			{
				String changedFieldName = change.getField_name();
				if (BugzillaStandardField.HOURS_WORKED.getFieldName().equals(changedFieldName))
					return true;
			}

			return false;
		}

		/**
		 * Constructs a new {@link BugzillaTaskWorkEntry} instance.
		 * 
		 * @param proxyEntry
		 *            the {@link HistoryEntry} proxy instance that this {@link BugzillaTaskWorkEntry}
		 *            represents
		 */
		public BugzillaTaskWorkEntry(HistoryEntry proxyEntry)
		{
			this.proxyEntry = proxyEntry;
		}

		/**
		 * @see com.google.code.jtasks.model.ITaskWorkEntry#getWorker()
		 */
		@Override
		public String getWorker()
		{
			return proxyEntry.getWho();
		}

		/**
		 * @see com.google.code.jtasks.model.ITaskWorkEntry#getCreationTime()
		 */
		@Override
		public DateTime getCreationTime()
		{
			return new DateTime(proxyEntry.getWhen());
		}

		/**
		 * @see com.google.code.jtasks.model.ITaskWorkEntry#getTimeWorked()
		 */
		@Override
		public Duration getTimeWorked()
		{
			for (Change change : proxyEntry.getChanges())
			{
				String changedFieldName = change.getField_name();
				if (BugzillaStandardField.HOURS_WORKED.getFieldName().equals(changedFieldName))
				{
					String hoursText = change.getAdded();
					double hours = Double.parseDouble(hoursText);
					long seconds = Math.round(hours * DateTimeConstants.SECONDS_PER_HOUR);
					return Duration.standardSeconds(seconds);
				}
			}

			throw new IllegalStateException(String.format("%s does not have a %s %s: %s", HistoryEntry.class,
					BugzillaStandardField.HOURS_WORKED, Change.class, proxyEntry));
		}

		/**
		 * @see com.google.code.jtasks.model.ITaskWorkEntry#getAssociatedComment()
		 */
		@Override
		public ITaskComment getAssociatedComment()
		{
			// TODO Auto-generated method stub
			return null;
		}
	}
}
